Problema

Usted trabaja para un organismo no gubernamental que está interesado en las dinámicas socioeconómicas que determinan la desigualdad de ingreso y la erradicación de la pobreza extrema, enmarcado dentro de los objetivos del desarrollo del nuevo milenio del PNUD. Le encomiendan el desarrollo de un modelo predictivo sobre la probabilidad que un individuo presente salarios por sobre o bajo los 50.000 dólares anuales, en base a una serie de atributos sociodemográficos.

Implementación del modelo

Dado que se trata de un problema de clasificación se implementará un modelo de regresión logística del tipo:

$$log \left( \frac{Pr(income=1)}{1-Prd(income=1)} \right) = \beta_0 + \sum_{i=0} {\beta }_{i} \cdot {X}_{i}$$

Donde:
  • ${X}_{i}$ corresponde a los atributos sociodemográficos
  • $income = 1$ individuo que percibe más de 50.000 dólares anuales y 0 si es menor o igual a 50.000

Métricas de desempeño

Machine Learning

Como es un modelo de clasificación las métricas a usar son:

Matriz de confusión: Tabla cruzada que permite evaluar la capacidad predictiva del modelo entre las etiquetas verdaderas y las predichas.

Accuracy: Porcentaje de casos predichos correctamente sobre el total de casos.

Precision: Medición de casos positivos predichos frente a las etiquetas positivas (VP & FP).

Recall: Fracción de casos positivos predichos por el modelo.

F1: Media armónica entre Precision y Recall

ROC: Evalúa la relación entre ambos errores condicional en todo el rango del clasificador

Descripción de la base de datos

Las variables que componen esta base se detallan a continuación:

agex: Edad del individuo.

workclass: Naturaleza de la organización que emplea al individuo.

education: Nivel educacional del individuo:Bachelors (Licenciado), Some-college (Superior incompleta), 11th (3ro medio), HS-grad (Secundaria completa), Prof-school (Escuela profesional), Assoc-acdm (Técnico superior administrativo) , Assoc-voc (Técnico superior vocacional), 9th (1ro medio), 7th-8th (7mo-8vo), 12th (4to medio), Masters (Maestría de postgrado), 1st-4th (1ro-4to básico), 10th(2do medio), Doctorate (Doctorado), 5th-6th (5to-6to), Preschool (Preescolar).

capital-gains: Ingresos generados por inversiones fuera del trabajo asalariado = Ingresos generados por inversiones fuera del trabajo asalariado.

capital-losses: Pérdidas generadas por inversiones fuera del trabajo asalariado.

fnlwgt: Ponderador muestral.

marital-status: Estado civil del individuo: Married-civ-spouse (Casado/a régimen civil), Divorced (Divorciado/a), Never-married (Soltero/a), Separated (Separado/a), Widowed (Viudo/a), Married-spouse-absent (Casado con esposo/a ausente), Married-AF-spouse (Casado/a régimen castrense).

occupation: Ocupación del individuo: Tech-support (Soporte técnico), Craft-repair (Reparaciones), Other-service (Otros servicios), Sales (Ventas), Exec-managerial (Ejecutivo administrativos), Prof-specialty (Profesores), Handlers cleaners (Aseo y ornato), Machine-op- inspct (Inspectores de maquinarias), Adm-clerical (Administrativos servicio al cliente), Farming- fishing (Pesca-ganadería), Transport-moving (Transporte), Priv-house-serv (Asesor del hogar), Protective-serv (servicios de seguridad), Armed-Forces (Fuerzas armadas).

relationship: Relación respecto a su familia Wife(Esposa), Own-child (hijo único), Husband (Esposo), Not-in-family (No pertenece a la familia), Other-relative (Familiar de otro tipo), Unmarried (Soltero).

race: Raza del encuestado White(Blanco caucásico), Asian-Pac-Islander (Isleño del Asia Pacífico), Amer-Indian-Eskimo (Pertenenciente a pueblos originarios), Other (Otro grupo), Black (Afroamericano).

sex: Sexo del encuestado.

Hours-per-week: Cantidad de horas trabajadas por semana.

native-country: País de origen. United-States, Cambodia, England, Puerto-Rico, Canada, Germany, Outlying-US(Guam USVI-etc), India, Japan, Greece, South, China, Cuba, Iran, Honduras, Philippines, Italy, Poland, Jamaica, Vietnam, Mexico, Portugal, Ireland, France, Dominican-Republic, Laos, Ecuador, Taiwan, Haiti, Columbia, Hungary, Guatemala, Nicaragua, Scotland, Thailand, Yugoslavia, El-Salvador, Trinadad&Tobago, Peru, Hong, Holand- Netherlands.

income: <=50K Si el individuo percibe ingresos inferiores a 50.000 dólares anuales, >50K si el individuo percibe ingresos superiores a 50.000 dólares anuales. Vector objetivo.

2. SECCIÓN ASPECTOS COMPUTACIONALES

Librerías y módulos

In [1]:
import pandas as pd
import pandas_profiling
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="ticks", palette = "pastel")
plt.style.use('seaborn') 

import scipy.stats as stats
import math

import statsmodels.formula.api as smf

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

from sklearn.metrics import classification_report, roc_curve,roc_auc_score

import missingno as msngo
import functions as funcs

from IPython.display import display, Markdown

import warnings
warnings.filterwarnings("ignore")

Proceso de preprocesamiento y recodificación de datos

  1. Exploración preliminar del tipo de datos que conforman los campos, como estadísticos generales de los mismos
  1. Transformar los valores perdidos dado que están como ?
  1. Recodificación de los siguientes campos:

    • occupation debe recodificarse como collars siguiendo una nomenclatura similar a:

      • white-collar $\leftarrow$ Prof specialty, Exec-managerial, Adm-clerical, Sales, Tech-support.

      • blue-collar $\leftarrow$ Craft-repair, Machine-op-inspct, Transport-moving,Handlers-cleaners, Farming fishing, Protective-serv, Priv-house-serv.

      • others $\leftarrow$ Other-service, Armed-Forces

  • workclass debe recodificarse como workclass_recod siguiendo una nomenclatura similar a:

    • federal-gov $\leftarrow$ Federal-gov.
    • state-level-gov $\leftarrow$ State-gov, Local-gov.
    • self-employed $\leftarrow$ Self-emp-inc, Self-emp-not-inc
    • unemployed $\leftarrow$ Never-worked, Without-pay.
  • education debe recodificarse como educ_recod siguiendo una nomenclatura similar a:

    • preschool $\leftarrow$ Preschool
    • elementary-school $\leftarrow$ 1st-4th, 5th-6th
    • high-school $\leftarrow$ 7th-8th, 9th, 10th,11th, 12th, HS-grad
    • college $\leftarrow$ Assoc-voc, Assoc-acdm, Some-college
    • university $\leftarrow$ Bachelors, Masters, Prof-school, Doctorate
  • marital-status debe recodificarse como civstatus siguiendo una nomenclatura similar a:

    • married $\leftarrow$ Married-civ-spouse, Married-spouse-absent, Married-AFspouse
    • divorced $\leftarrow$ Divorced
    • separated $\leftarrow$ Separated
    • widowed $\leftarrow$ Widowed.
  • native-country debe recodificarse como region donde cada país debe asignarse a uno de los 5 continentes.
  • income debe recodificarse de forma binaria
In [2]:
data = pd.read_csv('income-db.csv')
data.head()
Out[2]:
age workclass fnlwgt education educational-num marital-status occupation relationship race gender capital-gain capital-loss hours-per-week native-country income
0 25 Private 226802 11th 7 Never-married Machine-op-inspct Own-child Black Male 0 0 40 United-States <=50K
1 38 Private 89814 HS-grad 9 Married-civ-spouse Farming-fishing Husband White Male 0 0 50 United-States <=50K
2 28 Local-gov 336951 Assoc-acdm 12 Married-civ-spouse Protective-serv Husband White Male 0 0 40 United-States >50K
3 44 Private 160323 Some-college 10 Married-civ-spouse Machine-op-inspct Husband Black Male 7688 0 40 United-States >50K
4 18 ? 103497 Some-college 10 Never-married ? Own-child White Female 0 0 30 United-States <=50K
In [3]:
df = data.replace(['?','nan'],np.nan)
df.head()
Out[3]:
age workclass fnlwgt education educational-num marital-status occupation relationship race gender capital-gain capital-loss hours-per-week native-country income
0 25 Private 226802 11th 7 Never-married Machine-op-inspct Own-child Black Male 0 0 40 United-States <=50K
1 38 Private 89814 HS-grad 9 Married-civ-spouse Farming-fishing Husband White Male 0 0 50 United-States <=50K
2 28 Local-gov 336951 Assoc-acdm 12 Married-civ-spouse Protective-serv Husband White Male 0 0 40 United-States >50K
3 44 Private 160323 Some-college 10 Married-civ-spouse Machine-op-inspct Husband Black Male 7688 0 40 United-States >50K
4 18 NaN 103497 Some-college 10 Never-married NaN Own-child White Female 0 0 30 United-States <=50K

Revisión de valores perdidos

In [4]:
funcs.missing_values_table(df)
Out[4]:
Missing Values % Missing Values
occupation 2809 5.8
workclass 2799 5.7
native-country 857 1.8

3. SECCIÓN DESCRIPCIÓN

Recodificación de variables

Se procede a recodificar las siguientes variables

  • occupation
In [5]:
collar = {'Prof-specialty':'white_collar', 'Exec-managerial':'white_collar'
,'Adm-clerical':'white_collar', 'Sales':'white_collar', 'Tech-support':'white_collar'
,'Craft-repair':'blue_collar', 'Machine-op-inspct':'blue_collar'
,'Transport-moving':'blue_collar','Handlers-cleaners':'blue_collar'
,'Farming-fishing':'blue_collar', 'Protective-serv':'blue_collar'
,'Priv-house-serv':'blue_collar'
,'Other-service':'others','Armed-Forces':'others'}

df['occu_recod'] = df['occupation'].map(collar)
  • workclass $\rightarrow$ workclass_recod
In [6]:
workclass_recod = {'Federal-gov':'federal_gov','State-gov':'state_level_gov','Local-gov':'state_level_gov'
,'Self-emp-inc':'self_employed', 'Self-emp-not-inc':'self_employed'
,'Never-worked':'unemployed', 'Without-pay':'unemployed','Private':'private'}

df['workclass_recod'] = df['workclass'].map(workclass_recod)
  • education $\rightarrow$ educ_recod
In [7]:
educ_recod = {'Preschool':'preschool','1st-4th':'elementary_school', '5th-6th':'elementary_school'
,'7th-8th':'high_school','9th':'high_school','10th':'high_school','11th':'high_school'
,'12th':'high_school','HS-grad':'high_school'
,'Assoc-voc':'college','Assoc-acdm':'college','Some-college':'college'
,'Bachelors':'university', 'Masters':'university', 'Prof-school':'university','Doctorate':'university'}

df['educ_recod'] = df['education'].map(educ_recod)
  • marital-status $\rightarrow$ civstatus
In [8]:
civstatus = {'Married-civ-spouse':'married','Married-spouse-absent':'married'
,'Married-AFspouse':'married' ,'Divorced':'divorced','Separated':'separated'
,'Widowed':'widowed','Never-married':'never_married'}
df['civstatus'] = df['marital-status'].map(civstatus)
  • native-country $\rightarrow$ region
In [9]:
continent = {'United-States':'north_america', 'Canada':'north_america'
,'Peru':'latin_america_caribe', 'Guatemala':'latin_america_caribe'
,'Mexico':'latin_america_caribe','Dominican-Republic':'latin_america_caribe'
,'Haiti':'latin_america_caribe', 'El-Salvador':'latin_america_caribe', 'Puerto-Rico':'latin_america_caribe'
,'Nicaragua':'latin_america_caribe', 'Honduras':'latin_america_caribe', 'Jamaica':'latin_america_caribe'
,'Ecuador':'latin_america_caribe', 'Cuba':'latin_america_caribe','Columbia':'latin_america_caribe'
,'Trinadad&Tobago':'latin_america_caribe'
,'Ireland':'europe', 'Germany':'europe','Poland':'europe','England':'europe','Portugal':'europe'
,'Italy':'europe','Scotland':'europe', 'Yugoslavia':'europe', 'Hungary':'europe', 'Greece':'europe','France':'europe'
,'Holand-Netherlands':'europe'
,'Philippines':'asia_oceania','Thailand':'asia_oceania','India':'asia_oceania', 'Vietnam':'asia_oceania'
,'Japan':'asia_oceania','Cambodia':'asia_oceania'
,'South':'asia_oceania','Laos':'asia_oceania','Taiwan':'asia_oceania','China':'asia_oceania'
,'Iran':'asia_oceania','Hong':'asia_oceania','Outlying-US(Guam-USVI-etc)':'asia_oceania'}
 

df['region'] = df['native-country'].map(continent)
  • Se procede a binarizar la variable income
In [10]:
df['income'].value_counts('%')
Out[10]:
<=50K    0.760718
>50K     0.239282
Name: income, dtype: float64
In [11]:
df['income_binarize'] = np.where(df['income'] == '>50K', 1, 0).astype('int32')
In [12]:
df_analytics = df.drop(columns=['occupation', 'workclass', 'education', 'marital-status'])
In [13]:
prof = pandas_profiling.ProfileReport(df_analytics)
prof.to_file(output_file='output.html')
prof




Out[13]:

Observaciones

  1. Dado que no se puede determinar si están duplicados los datos, pues están anonimizados, de igual modo se eliminarán pues el impacto afecta solo al 0,2% de la data
  1. El 0,8% de la data está con valores perdidos concentrándose en las variables occu_recod, native-country y workclass_recod

  2. Entendiendo que capital-gains y capital-loss tienen una alta tasa de valores ceros, estos serán eliminados

  1. Los tramos educacionales presentan 2 categorías con un n de casos muy bajos por lo que existe una posibilidad de reagrupar elementary, highschool y preschool en una variable tipo mandatory education
  1. Como no hay una definición clara del campo fnlwgt y su cálculo sobre la muestra se eliminará

  2. el 50% de la variable age se concentra entre los 17 y 37 años por lo que es posible realizar una normalización de la misma, dado que es asintótica a la izquierda

Eliminación data duplicada y variables con datos ceros

In [14]:
df_analytics2 = df_analytics.drop_duplicates()
In [15]:
df_analytics3 = df_analytics2.drop(columns=['capital-gain', 'capital-loss','fnlwgt'])
df_analytics4 = df_analytics3.dropna()
In [16]:
df_analytics4.rename(columns=lambda x: x.replace('occu_recod', 'collars'), inplace=True)
In [17]:
df_analytics4.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 45098 entries, 0 to 48841
Data columns (total 14 columns):
 #   Column           Non-Null Count  Dtype 
---  ------           --------------  ----- 
 0   age              45098 non-null  int64 
 1   educational-num  45098 non-null  int64 
 2   relationship     45098 non-null  object
 3   race             45098 non-null  object
 4   gender           45098 non-null  object
 5   hours-per-week   45098 non-null  int64 
 6   native-country   45098 non-null  object
 7   income           45098 non-null  object
 8   collars          45098 non-null  object
 9   workclass_recod  45098 non-null  object
 10  educ_recod       45098 non-null  object
 11  civstatus        45098 non-null  object
 12  region           45098 non-null  object
 13  income_binarize  45098 non-null  int32 
dtypes: int32(1), int64(3), object(10)
memory usage: 5.0+ MB

HITO 2: Exploración visual de la data

Barplots

In [18]:
categorical = ['relationship', 'race', 'gender', 'region', 'collars'
               , 'workclass_recod', 'educ_recod','civstatus','income']


funcs.countplot_data(df_analytics4[categorical])

Observaciones

relationship

  • 41% de los encuestados son esposos dentro en la relación

  • 37% no conforma una familia o no está casado

race

  • 95% de la categoría se distribuye entre white y black

gender

  • 1/3 de la muestra corresponde a mujeres

region

  • Se observa que la base está desbalanceada, dado que el 92% son de norteamérica

collars

  • 54% de la base es white-collar, mientras que los blue-collar 35%

workclass_recod

  • el 68% trabaja de manera dependiente para una institución privada, mientras que el 12% es independiente y el 11% es trabajador público

educ_recod

  • 44% tiene sus estudios secundarios completos

  • 30% tiene college

  • 25% tiene estudios universitarios o de posgrado

civstatus

  • 32% nunca se ha caso, mientras 48% sí lo ha hecho

  • 14% está divorciado

  • Dada la poca proporción de la categoría separated es conviente agruparla con divorced

income

  • 1/4 de los encuestados recibe 50 mil dólares al año
In [19]:
categorical_income = ['relationship', 'race', 'gender', 'region', 'collars'
                      , 'workclass_recod', 'educ_recod','civstatus']


fig, ax = plt.subplots(2,4, figsize=(25,12))
for variable, subplot in zip(categorical_income, ax.flatten()):
    y = df_analytics4.groupby(variable)["income"].value_counts(normalize=True).unstack()
    y.plot(kind="bar", stacked="True", ax = subplot)
    sns.set(style="ticks", palette = "pastel")

Observaciones

relationship

  • Se tiene que tanto las esposas como los maridos se distribuyen en un 50% con ingresos superiores a los 50 mil dólares respectivamente
  • Los otros tipos de relaciones sobre el 90% reciben menos de 50 mil anual

race

  • Las personas de Asia Pacífico y blancos alcanzan un 30% de ingresos anuales superiores a 50 mil dólares, los restantes solo un 10%

gender

  • Solo el 10% de las mujeres percibe más de 50 mil dólares, mientras que los hombres llegan a un 30%

region

  • El 30% de los asiaticos y europeos recibe más de 50 mil dólares, mientras que los norteamericanos son el 25%

collars

  • Cerca del 35% de los white-collar recibe más de 50.000 dólares > trabajos más calificados

workclass_recod

  • Los trabajadores públicos (federales y locales) tienen un grupo con mayores ingresos anuales junto con los independientes

educ_recod

  • A mayor nivel educacional mayor ingreso anual. El 50% de los universitarios tienen mayores ingresos

civstatus

  • Los casados tienen la población con más personas con ingresos altos 45%

Boxplots

Comparación variable age vs otras variables categóricas

In [20]:
funcs.boxplot_data(df_analytics4,categorical,'age')

Observaciones

  • A mayor edad mayor ingreso superior a 50 mil anual, entre 41 - 45 años promedio, mientras que el otro grupo oscila los 35
  • La edad entre los tipos de raza tiende a ser más homogénea, al igual de de las regiones y tipos de trabajo

Comparación variable hours-per-week vs otras variables categóricas

In [21]:
funcs.boxplot_data(df_analytics4,categorical,'hours-per-week')

Observaciones

  • En términos generales se observa que entre en todos las categorias hay una fuerte dispersión con respecto a la variable horas trabajadas durante la semana, ahora bien hay algunas categorías que son más dispersas pues se ven afectadas por el poco peso que representan dentro de su categoría (relationship, region, race)
  • En relación al vector objetivo income se observa que para los que ganan a lo más 50 mil anual sus horas trabajadas son más dispersas, lo que podría suponer hay que trabajar más horas para obtener un ingreso mayor, o bien, el trabajo en el que se está es part time, proyectos, etc

Análisis variable age y su transformación logarítmica

In [22]:
df_analytics4['age_norm'] = np.log(df_analytics4['age'])
In [23]:
numerical_norm= ['age', 'age_norm']

fig, ax = plt.subplots(1, 2, figsize=(8, 4))
for var, subplot in zip(numerical_norm, ax.flatten()):
    sns.distplot(df_analytics4[var], ax=subplot)
    fig.suptitle('Comparación entre la variable age y su normalización')
  • Observando la normalización de la edad, se plantea la posibilidad de categorizarla, para reducir un poco lo asintótica que es la curca
In [24]:
age_rank = [-math.inf,30,44,math.inf]
categorias = ['entry','mature','senior']
df_analytics4['categorical_age'] = pd.cut(x = df_analytics4['age'], bins = age_rank, labels = categorias)
df_analytics4['categorical_age'].value_counts('%')
Out[24]:
mature    0.369041
senior    0.316422
entry     0.314537
Name: categorical_age, dtype: float64
In [25]:
df_analytics4.head()
Out[25]:
age educational-num relationship race gender hours-per-week native-country income collars workclass_recod educ_recod civstatus region income_binarize age_norm categorical_age
0 25 7 Own-child Black Male 40 United-States <=50K blue_collar private high_school never_married north_america 0 3.218876 entry
1 38 9 Husband White Male 50 United-States <=50K blue_collar private high_school married north_america 0 3.637586 mature
2 28 12 Husband White Male 40 United-States >50K blue_collar state_level_gov college married north_america 1 3.332205 entry
3 44 10 Husband Black Male 40 United-States >50K blue_collar private college married north_america 1 3.784190 mature
5 34 6 Not-in-family White Male 30 United-States <=50K others private high_school never_married north_america 0 3.526361 mature

Análisis variable hours-per-week y binarización

In [26]:
sns.kdeplot(df_analytics4['hours-per-week']);
  • Dado que los horas semanales se concentran en torno a las 40 horas es preferible binarizarla
In [27]:
df_analytics4['hpw'] = np.where(df_analytics4['hours-per-week'] <= 40, 1, 0)
df_analytics4['hpw'].value_counts('%')
Out[27]:
1    0.695064
0    0.304936
Name: hpw, dtype: float64

Recodificación final de campos

educ_recod

  • Se recodificará en las siguientes categorías:
* mandatory_school: preeschool, elementary-school, highschool
* college
* university
In [28]:
educ_recod_2 = {'preschool':'mandatory_school','elementary_school':'mandatory_school', 'high_school':'mandatory_school'
,'college':'college','university':'university'}

df_analytics4['educ_recod_final'] = df['educ_recod'].map(educ_recod_2)

df_analytics4.head(5)
Out[28]:
age educational-num relationship race gender hours-per-week native-country income collars workclass_recod educ_recod civstatus region income_binarize age_norm categorical_age hpw educ_recod_final
0 25 7 Own-child Black Male 40 United-States <=50K blue_collar private high_school never_married north_america 0 3.218876 entry 1 mandatory_school
1 38 9 Husband White Male 50 United-States <=50K blue_collar private high_school married north_america 0 3.637586 mature 0 mandatory_school
2 28 12 Husband White Male 40 United-States >50K blue_collar state_level_gov college married north_america 1 3.332205 entry 1 college
3 44 10 Husband Black Male 40 United-States >50K blue_collar private college married north_america 1 3.784190 mature 1 college
5 34 6 Not-in-family White Male 30 United-States <=50K others private high_school never_married north_america 0 3.526361 mature 1 mandatory_school

civstatus

  • Se recodificará en las siguientes categorías:
* married-widowed: widowed, married
* separated: separated,divorced
* single: never-married
In [29]:
civstatus_recod = {'widowed':'married_widowed','separated':'separated', 'divorced':'separated'
,'never_married':'single','married':'married_widowed'}

df_analytics4['civstatus_recod'] = df_analytics4['civstatus'].map(civstatus_recod)

df_analytics4.head()
Out[29]:
age educational-num relationship race gender hours-per-week native-country income collars workclass_recod educ_recod civstatus region income_binarize age_norm categorical_age hpw educ_recod_final civstatus_recod
0 25 7 Own-child Black Male 40 United-States <=50K blue_collar private high_school never_married north_america 0 3.218876 entry 1 mandatory_school single
1 38 9 Husband White Male 50 United-States <=50K blue_collar private high_school married north_america 0 3.637586 mature 0 mandatory_school married_widowed
2 28 12 Husband White Male 40 United-States >50K blue_collar state_level_gov college married north_america 1 3.332205 entry 1 college married_widowed
3 44 10 Husband Black Male 40 United-States >50K blue_collar private college married north_america 1 3.784190 mature 1 college married_widowed
5 34 6 Not-in-family White Male 30 United-States <=50K others private high_school never_married north_america 0 3.526361 mature 1 mandatory_school single

race

  • Se recodificará en las siguientes categorías:
* Black: Black
* White: White
* Others: Other, Amer-Indian-Eskimo, Asian-Pac-Islander
In [30]:
race_recod = {'Black':'black','White':'white','Amer-Indian-Eskimo':'others'
             ,'Asian-Pac-Islander':'others','Other':'others'}

df_analytics4['race_recod'] = df_analytics4['race'].map(race_recod)

df_analytics4.head()
Out[30]:
age educational-num relationship race gender hours-per-week native-country income collars workclass_recod educ_recod civstatus region income_binarize age_norm categorical_age hpw educ_recod_final civstatus_recod race_recod
0 25 7 Own-child Black Male 40 United-States <=50K blue_collar private high_school never_married north_america 0 3.218876 entry 1 mandatory_school single black
1 38 9 Husband White Male 50 United-States <=50K blue_collar private high_school married north_america 0 3.637586 mature 0 mandatory_school married_widowed white
2 28 12 Husband White Male 40 United-States >50K blue_collar state_level_gov college married north_america 1 3.332205 entry 1 college married_widowed white
3 44 10 Husband Black Male 40 United-States >50K blue_collar private college married north_america 1 3.784190 mature 1 college married_widowed black
5 34 6 Not-in-family White Male 30 United-States <=50K others private high_school never_married north_america 0 3.526361 mature 1 mandatory_school single white
In [31]:
variable_recod = ['race','race_recod','educ_recod','educ_recod_final'
                  ,'civstatus','civstatus_recod']
In [32]:
funcs.countplot_data(df_analytics4[variable_recod])
In [33]:
df_analytics4.head()
Out[33]:
age educational-num relationship race gender hours-per-week native-country income collars workclass_recod educ_recod civstatus region income_binarize age_norm categorical_age hpw educ_recod_final civstatus_recod race_recod
0 25 7 Own-child Black Male 40 United-States <=50K blue_collar private high_school never_married north_america 0 3.218876 entry 1 mandatory_school single black
1 38 9 Husband White Male 50 United-States <=50K blue_collar private high_school married north_america 0 3.637586 mature 0 mandatory_school married_widowed white
2 28 12 Husband White Male 40 United-States >50K blue_collar state_level_gov college married north_america 1 3.332205 entry 1 college married_widowed white
3 44 10 Husband Black Male 40 United-States >50K blue_collar private college married north_america 1 3.784190 mature 1 college married_widowed black
5 34 6 Not-in-family White Male 30 United-States <=50K others private high_school never_married north_america 0 3.526361 mature 1 mandatory_school single white

Creación de dummies

In [34]:
df_analytics4['sex'] = np.where(df_analytics4['gender'] == 'Male', 1, 0)
variables_dummy = ['relationship','race_recod','collars','educ_recod_final','civstatus_recod'
                  ,'workclass_recod','categorical_age','region']
In [35]:
data_model_preproc = funcs.createDummies(df_analytics4,variables_dummy)
In [36]:
data_model_preproc.head()
Out[36]:
age educational-num race gender hours-per-week native-country income educ_recod civstatus income_binarize ... workclass_recod_self_employed workclass_recod_state_level_gov workclass_recod_unemployed categorical_age_entry categorical_age_mature categorical_age_senior region_asia_oceania region_europe region_latin_america_caribe region_north_america
0 25 7 Black Male 40 United-States <=50K high_school never_married 0 ... 0 0 0 1 0 0 0 0 0 1
1 38 9 White Male 50 United-States <=50K high_school married 0 ... 0 0 0 0 1 0 0 0 0 1
2 28 12 White Male 40 United-States >50K college married 1 ... 0 1 0 1 0 0 0 0 0 1
3 44 10 Black Male 40 United-States >50K college married 1 ... 0 0 0 0 1 0 0 0 0 1
5 34 6 White Male 30 United-States <=50K high_school never_married 0 ... 0 0 0 0 1 0 0 0 0 1

5 rows × 43 columns

In [37]:
data_model_preproc.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 45098 entries, 0 to 48841
Data columns (total 43 columns):
 #   Column                             Non-Null Count  Dtype  
---  ------                             --------------  -----  
 0   age                                45098 non-null  int64  
 1   educational-num                    45098 non-null  int64  
 2   race                               45098 non-null  object 
 3   gender                             45098 non-null  object 
 4   hours-per-week                     45098 non-null  int64  
 5   native-country                     45098 non-null  object 
 6   income                             45098 non-null  object 
 7   educ_recod                         45098 non-null  object 
 8   civstatus                          45098 non-null  object 
 9   income_binarize                    45098 non-null  int32  
 10  age_norm                           45098 non-null  float64
 11  hpw                                45098 non-null  int64  
 12  sex                                45098 non-null  int64  
 13  relationship_Husband               45098 non-null  int32  
 14  relationship_Not-in-family         45098 non-null  int32  
 15  relationship_Other-relative        45098 non-null  int32  
 16  relationship_Own-child             45098 non-null  int32  
 17  relationship_Unmarried             45098 non-null  int32  
 18  relationship_Wife                  45098 non-null  int32  
 19  race_recod_black                   45098 non-null  int32  
 20  race_recod_others                  45098 non-null  int32  
 21  race_recod_white                   45098 non-null  int32  
 22  collars_blue_collar                45098 non-null  int32  
 23  collars_others                     45098 non-null  int32  
 24  collars_white_collar               45098 non-null  int32  
 25  educ_recod_final_college           45098 non-null  int32  
 26  educ_recod_final_mandatory_school  45098 non-null  int32  
 27  educ_recod_final_university        45098 non-null  int32  
 28  civstatus_recod_married_widowed    45098 non-null  int32  
 29  civstatus_recod_separated          45098 non-null  int32  
 30  civstatus_recod_single             45098 non-null  int32  
 31  workclass_recod_federal_gov        45098 non-null  int32  
 32  workclass_recod_private            45098 non-null  int32  
 33  workclass_recod_self_employed      45098 non-null  int32  
 34  workclass_recod_state_level_gov    45098 non-null  int32  
 35  workclass_recod_unemployed         45098 non-null  int32  
 36  categorical_age_entry              45098 non-null  int32  
 37  categorical_age_mature             45098 non-null  int32  
 38  categorical_age_senior             45098 non-null  int32  
 39  region_asia_oceania                45098 non-null  int32  
 40  region_europe                      45098 non-null  int32  
 41  region_latin_america_caribe        45098 non-null  int32  
 42  region_north_america               45098 non-null  int32  
dtypes: float64(1), int32(31), int64(5), object(6)
memory usage: 11.1+ MB
In [38]:
data_model_preproc.rename(columns=lambda x: x.replace('relationship_Husband','relationship_husband'), inplace=True)
data_model_preproc.rename(columns=lambda x: x.replace('relationship_Not-in-family','relationship_not_in_family'), inplace=True)
data_model_preproc.rename(columns=lambda x: x.replace('relationship_Other-relative','relationship_other_relative'), inplace=True)
data_model_preproc.rename(columns=lambda x: x.replace('relationship_Own-child','relationship_own_child'), inplace=True)
data_model_preproc.rename(columns=lambda x: x.replace('relationship_Unmarried','relationship_unmarried'), inplace=True)
data_model_preproc.rename(columns=lambda x: x.replace('relationship_Wife','relationship_wife'), inplace=True)

data_model_preproc.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 45098 entries, 0 to 48841
Data columns (total 43 columns):
 #   Column                             Non-Null Count  Dtype  
---  ------                             --------------  -----  
 0   age                                45098 non-null  int64  
 1   educational-num                    45098 non-null  int64  
 2   race                               45098 non-null  object 
 3   gender                             45098 non-null  object 
 4   hours-per-week                     45098 non-null  int64  
 5   native-country                     45098 non-null  object 
 6   income                             45098 non-null  object 
 7   educ_recod                         45098 non-null  object 
 8   civstatus                          45098 non-null  object 
 9   income_binarize                    45098 non-null  int32  
 10  age_norm                           45098 non-null  float64
 11  hpw                                45098 non-null  int64  
 12  sex                                45098 non-null  int64  
 13  relationship_husband               45098 non-null  int32  
 14  relationship_not_in_family         45098 non-null  int32  
 15  relationship_other_relative        45098 non-null  int32  
 16  relationship_own_child             45098 non-null  int32  
 17  relationship_unmarried             45098 non-null  int32  
 18  relationship_wife                  45098 non-null  int32  
 19  race_recod_black                   45098 non-null  int32  
 20  race_recod_others                  45098 non-null  int32  
 21  race_recod_white                   45098 non-null  int32  
 22  collars_blue_collar                45098 non-null  int32  
 23  collars_others                     45098 non-null  int32  
 24  collars_white_collar               45098 non-null  int32  
 25  educ_recod_final_college           45098 non-null  int32  
 26  educ_recod_final_mandatory_school  45098 non-null  int32  
 27  educ_recod_final_university        45098 non-null  int32  
 28  civstatus_recod_married_widowed    45098 non-null  int32  
 29  civstatus_recod_separated          45098 non-null  int32  
 30  civstatus_recod_single             45098 non-null  int32  
 31  workclass_recod_federal_gov        45098 non-null  int32  
 32  workclass_recod_private            45098 non-null  int32  
 33  workclass_recod_self_employed      45098 non-null  int32  
 34  workclass_recod_state_level_gov    45098 non-null  int32  
 35  workclass_recod_unemployed         45098 non-null  int32  
 36  categorical_age_entry              45098 non-null  int32  
 37  categorical_age_mature             45098 non-null  int32  
 38  categorical_age_senior             45098 non-null  int32  
 39  region_asia_oceania                45098 non-null  int32  
 40  region_europe                      45098 non-null  int32  
 41  region_latin_america_caribe        45098 non-null  int32  
 42  region_north_america               45098 non-null  int32  
dtypes: float64(1), int32(31), int64(5), object(6)
memory usage: 11.1+ MB

HITO 3:

4. SECCIÓN MODELACIÓN DESCRIPTIVA

Selección de regresores

  • Para evitar multicolinealidad se excluirán las categorías más grandes, por lo mismo la variable relationship no será considerada en el modelo, dado que hay categorías que pueden ser medidas con civstatus
In [39]:
regresores = list(data_model_preproc.columns)

exclusiones = ['age','categorical_age_mature','educational-num','race','gender','hours-per-week','native-country'
               ,'income','educ_recod','civstatus','region','income_binarize','age_norm','race_recod_white'
               ,'region_north_america','relationship_husband','civstatus_recod_married_widowed'
               ,'collars_white_collar','workclass_recod_private','educ_recod_final_mandatory_school'
               ,'relationship_not_in_family','relationship_other_relative','relationship_own_child'
               ,'relationship_unmarried','relationship_wife']

regresores = [e for e in regresores if e not in list(exclusiones)]

inputs = ' + '.join(regresores)

modelo = 'income_binarize ~ ' + inputs
In [40]:
logit1 = smf.logit(modelo,data_model_preproc).fit()
logit1.summary2()
Optimization terminated successfully.
         Current function value: 0.375956
         Iterations 8
Out[40]:
Model: Logit Pseudo R-squared: 0.329
Dependent Variable: income_binarize AIC: 33949.7693
Date: 2020-08-10 01:39 BIC: 34124.1012
No. Observations: 45098 Log-Likelihood: -16955.
Df Model: 19 LL-Null: -25257.
Df Residuals: 45078 LLR p-value: 0.0000
Converged: 1.0000 Scale: 1.0000
No. Iterations: 8.0000
Coef. Std.Err. z P>|z| [0.025 0.975]
Intercept -0.5468 0.0504 -10.8448 0.0000 -0.6457 -0.4480
hpw -0.6493 0.0286 -22.6697 0.0000 -0.7055 -0.5932
sex 0.5924 0.0372 15.9129 0.0000 0.5194 0.6653
race_recod_black -0.1819 0.0573 -3.1742 0.0015 -0.2942 -0.0696
race_recod_others -0.0983 0.0901 -1.0920 0.2748 -0.2749 0.0782
collars_blue_collar -0.6921 0.0333 -20.7687 0.0000 -0.7574 -0.6267
collars_others -1.4828 0.0803 -18.4665 0.0000 -1.6402 -1.3254
educ_recod_final_college 0.5891 0.0345 17.0580 0.0000 0.5214 0.6567
educ_recod_final_university 1.5886 0.0375 42.3497 0.0000 1.5151 1.6622
civstatus_recod_separated -1.8861 0.0462 -40.8175 0.0000 -1.9767 -1.7956
civstatus_recod_single -2.2787 0.0474 -48.0307 0.0000 -2.3717 -2.1857
workclass_recod_federal_gov 0.4560 0.0708 6.4368 0.0000 0.3171 0.5948
workclass_recod_self_employed -0.2336 0.0386 -6.0559 0.0000 -0.3092 -0.1580
workclass_recod_state_level_gov -0.0708 0.0430 -1.6466 0.0996 -0.1550 0.0135
workclass_recod_unemployed -1.1623 0.7859 -1.4789 0.1392 -2.7026 0.3781
categorical_age_entry -1.0797 0.0442 -24.4551 0.0000 -1.1663 -0.9932
categorical_age_senior 0.1406 0.0298 4.7251 0.0000 0.0823 0.1989
region_asia_oceania -0.2298 0.1163 -1.9754 0.0482 -0.4578 -0.0018
region_europe 0.1228 0.0993 1.2366 0.2162 -0.0718 0.3174
region_latin_america_caribe -0.8934 0.0929 -9.6126 0.0000 -1.0756 -0.7113
In [41]:
funcs.significant_pvalues(logit1)
Out[41]:
hpw                              8.917279e-114
sex                               5.155180e-57
race_recod_black                  1.502390e-03
collars_blue_collar               8.304681e-96
collars_others                    3.842192e-76
educ_recod_final_college          3.047458e-65
educ_recod_final_university       0.000000e+00
civstatus_recod_separated         0.000000e+00
civstatus_recod_single            0.000000e+00
workclass_recod_federal_gov       1.220248e-10
workclass_recod_self_employed     1.396392e-09
categorical_age_entry            4.439406e-132
categorical_age_senior            2.299835e-06
region_asia_oceania               4.822043e-02
region_latin_america_caribe       7.077291e-22
dtype: float64
In [42]:
exclusiones_2 = list(funcs.not_significant_pvalues(logit1).index)
exclusiones_2
Out[42]:
['race_recod_others',
 'workclass_recod_state_level_gov',
 'workclass_recod_unemployed',
 'region_europe']
In [43]:
regresores2 = [e for e in regresores if e not in list(exclusiones_2)]
regresores2

inputs2 = ' + '.join(regresores2)

modelo2 = 'income_binarize ~ ' + inputs2
In [44]:
logit2 = smf.logit(modelo2,data_model_preproc).fit()
logit2.summary2()
Optimization terminated successfully.
         Current function value: 0.376049
         Iterations 8
Out[44]:
Model: Logit Pseudo R-squared: 0.329
Dependent Variable: income_binarize AIC: 33950.1383
Date: 2020-08-10 01:39 BIC: 34089.6038
No. Observations: 45098 Log-Likelihood: -16959.
Df Model: 15 LL-Null: -25257.
Df Residuals: 45082 LLR p-value: 0.0000
Converged: 1.0000 Scale: 1.0000
No. Iterations: 8.0000
Coef. Std.Err. z P>|z| [0.025 0.975]
Intercept -0.5549 0.0502 -11.0630 0.0000 -0.6532 -0.4566
hpw -0.6524 0.0286 -22.8310 0.0000 -0.7084 -0.5964
sex 0.5964 0.0372 16.0448 0.0000 0.5236 0.6693
race_recod_black -0.1844 0.0572 -3.2247 0.0013 -0.2965 -0.0723
collars_blue_collar -0.6928 0.0333 -20.7976 0.0000 -0.7580 -0.6275
collars_others -1.4840 0.0803 -18.4870 0.0000 -1.6413 -1.3266
educ_recod_final_college 0.5852 0.0345 16.9736 0.0000 0.5176 0.6528
educ_recod_final_university 1.5805 0.0371 42.6424 0.0000 1.5078 1.6531
civstatus_recod_separated -1.8856 0.0462 -40.8075 0.0000 -1.9762 -1.7950
civstatus_recod_single -2.2781 0.0474 -48.0299 0.0000 -2.3711 -2.1852
workclass_recod_federal_gov 0.4658 0.0704 6.6197 0.0000 0.3279 0.6037
workclass_recod_self_employed -0.2214 0.0380 -5.8257 0.0000 -0.2959 -0.1469
categorical_age_entry -1.0774 0.0441 -24.4259 0.0000 -1.1638 -0.9909
categorical_age_senior 0.1390 0.0297 4.6767 0.0000 0.0807 0.1972
region_asia_oceania -0.3146 0.0872 -3.6078 0.0003 -0.4855 -0.1437
region_latin_america_caribe -0.8979 0.0927 -9.6861 0.0000 -1.0796 -0.7162
In [45]:
prob_params = round((100 * logit2.params[1:] / 4).sort_values(),3)
prob_params
Out[45]:
civstatus_recod_single          -56.953
civstatus_recod_separated       -47.140
collars_others                  -37.099
categorical_age_entry           -26.934
region_latin_america_caribe     -22.449
collars_blue_collar             -17.319
hpw                             -16.311
region_asia_oceania              -7.864
workclass_recod_self_employed    -5.535
race_recod_black                 -4.610
categorical_age_senior            3.474
workclass_recod_federal_gov      11.645
educ_recod_final_college         14.630
sex                              14.910
educ_recod_final_university      39.512
dtype: float64

Comentarios

In [46]:
for index, value in enumerate(prob_params):
    print('* Si el individuo forma parte del grupo {},{} la probabilidad de percibir más de 50.000 USD variaría en {}% {}'
          .format(prob_params.keys()[index],'\n',round(value,1),'\n'))
* Si el individuo forma parte del grupo civstatus_recod_single,
 la probabilidad de percibir más de 50.000 USD variaría en -57.0% 

* Si el individuo forma parte del grupo civstatus_recod_separated,
 la probabilidad de percibir más de 50.000 USD variaría en -47.1% 

* Si el individuo forma parte del grupo collars_others,
 la probabilidad de percibir más de 50.000 USD variaría en -37.1% 

* Si el individuo forma parte del grupo categorical_age_entry,
 la probabilidad de percibir más de 50.000 USD variaría en -26.9% 

* Si el individuo forma parte del grupo region_latin_america_caribe,
 la probabilidad de percibir más de 50.000 USD variaría en -22.4% 

* Si el individuo forma parte del grupo collars_blue_collar,
 la probabilidad de percibir más de 50.000 USD variaría en -17.3% 

* Si el individuo forma parte del grupo hpw,
 la probabilidad de percibir más de 50.000 USD variaría en -16.3% 

* Si el individuo forma parte del grupo region_asia_oceania,
 la probabilidad de percibir más de 50.000 USD variaría en -7.9% 

* Si el individuo forma parte del grupo workclass_recod_self_employed,
 la probabilidad de percibir más de 50.000 USD variaría en -5.5% 

* Si el individuo forma parte del grupo race_recod_black,
 la probabilidad de percibir más de 50.000 USD variaría en -4.6% 

* Si el individuo forma parte del grupo categorical_age_senior,
 la probabilidad de percibir más de 50.000 USD variaría en 3.5% 

* Si el individuo forma parte del grupo workclass_recod_federal_gov,
 la probabilidad de percibir más de 50.000 USD variaría en 11.6% 

* Si el individuo forma parte del grupo educ_recod_final_college,
 la probabilidad de percibir más de 50.000 USD variaría en 14.6% 

* Si el individuo forma parte del grupo sex,
 la probabilidad de percibir más de 50.000 USD variaría en 14.9% 

* Si el individuo forma parte del grupo educ_recod_final_university,
 la probabilidad de percibir más de 50.000 USD variaría en 39.5% 

HITO 4:

4. SECCIÓN MODELACIÓN PREDICTIVA

Generación de subconjunto de entrenamiento y validación

  • Para efectos del análisis se usarán como atributos los regresores que en el Hito 3 fueron estadísticamente significativos

  • Se trabajará con la librería scikit-learn y en particular LogisticRegression

  • Se calcularán 3 modelos:

    1. Con intercepto y sin balancear
    2. Con intercepto y balanceado
    3. Sin intercepto y balanceado

Estandarización de los atributos

In [47]:
data_to_predict = ['income_binarize','civstatus_recod_single','civstatus_recod_separated'
                       ,'collars_others','categorical_age_entry','region_latin_america_caribe','collars_blue_collar','hpw','region_asia_oceania'
                       ,'workclass_recod_self_employed','race_recod_black'
                       ,'categorical_age_senior','workclass_recod_federal_gov','educ_recod_final_college'
                       ,'sex','educ_recod_final_university']


data_model = data_model_preproc[data_to_predict]
data_model.head()
Out[47]:
income_binarize civstatus_recod_single civstatus_recod_separated collars_others categorical_age_entry region_latin_america_caribe collars_blue_collar hpw region_asia_oceania workclass_recod_self_employed race_recod_black categorical_age_senior workclass_recod_federal_gov educ_recod_final_college sex educ_recod_final_university
0 0 1 0 0 1 0 1 1 0 0 1 0 0 0 1 0
1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0
2 1 0 0 0 1 0 1 1 0 0 0 0 0 1 1 0
3 1 0 0 0 0 0 1 1 0 0 1 0 0 1 1 0
5 0 1 0 1 0 0 0 1 0 0 0 0 0 0 1 0
In [48]:
attributes = list(data_to_predict)
attributes.remove('income_binarize')
target = ['income_binarize']
In [49]:
X_train, X_test, Y_train, Y_test = train_test_split(data_model.loc[:, attributes],data_model.loc[:,target], test_size=0.33, random_state=11238)

X_train_std = StandardScaler().fit_transform(X_train)
X_test_std = StandardScaler().fit_transform(X_test)

Con intercepto y sin balancear

In [50]:
model_1 = LogisticRegression().fit(X_train_std, Y_train)
yhat = model_1.predict(X_test_std)
yhat_prob_1 = model_1.predict_proba(X_test_std)[:, 1]


metric_table_1 = pd.DataFrame(classification_report(Y_test,yhat,output_dict=True))
display(metric_table_1)
print('\nAUC:', round(roc_auc_score(Y_test, yhat_prob_1),5), '\n')
0 1 accuracy macro avg weighted avg
precision 0.843860 0.700832 0.818451 0.772346 0.807687
recall 0.928861 0.492295 0.818451 0.710578 0.818451
f1-score 0.884322 0.578340 0.818451 0.731331 0.806937
support 11119.000000 3764.000000 0.818451 14883.000000 14883.000000
AUC: 0.86642 

Con intercepto y balanceado

In [51]:
model_2 = LogisticRegression(class_weight='balanced').fit(X_train_std, Y_train)
yhat_2 = model_2.predict(X_test_std)
yhat_prob_2 = model_2.predict_proba(X_test_std)[:, 1]

metric_table_2 = pd.DataFrame(classification_report(Y_test,yhat_2,output_dict=True))
display(metric_table_2)
print('\nAUC:', round(roc_auc_score(Y_test, yhat_prob_2),5), '\n')
0 1 accuracy macro avg weighted avg
precision 0.913501 0.546840 0.780958 0.730171 0.820770
recall 0.780736 0.781615 0.780958 0.781175 0.780958
f1-score 0.841916 0.643482 0.780958 0.742699 0.791731
support 11119.000000 3764.000000 0.780958 14883.000000 14883.000000
AUC: 0.86644 

Sin intercepto y sin balancear

In [52]:
model_3 = LogisticRegression(fit_intercept=False, class_weight='balanced').fit(X_train_std, Y_train)
yhat_3 = model_3.predict(X_test_std)
yhat_prob_3 = model_3.predict_proba(X_test_std)[:, 1]


metric_table_3 = pd.DataFrame(classification_report(Y_test,yhat_3,output_dict=True))
display(metric_table_3)
print('\nAUC:', round(roc_auc_score(Y_test, yhat_prob_3),5), '\n')
0 1 accuracy macro avg weighted avg
precision 0.958974 0.445828 0.690385 0.702401 0.829196
recall 0.611746 0.922689 0.690385 0.767217 0.690385
f1-score 0.746980 0.601177 0.690385 0.674079 0.710106
support 11119.000000 3764.000000 0.690385 14883.000000 14883.000000
AUC: 0.86574 

Comentarios

  • Observando los resultados de las métricas de recall (0,68) y f1-score (0,64) el modelo_2 es el que tiene mejor desempeño entre los 3, además su accuracy (0,78) es bastante bueno y su precision aceptable (0,54)

  • Además el AUC es levemente superior en el modelo_2 por sobre los otros 2

  • Los 3 modelos predicen mucho mejor que un clasificador random la probabilidad de que un individuo perciba más de 50 mil dólares al año

Curva ROC - model_2

In [53]:
funcs.plot_roc_curve(Y_test,yhat_prob_2)
In [ ]: